package proc

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"log"
	"os"
	"os/exec"
	"strconv"
	"strings"
	"sync"
	"syscall"
	"time"
)

// Cmd 命令
type Cmd struct {
	name    string
	path    string
	args    []string
	dir     string
	env     []string
	stdout  io.Writer
	stderr  io.Writer
	stdin   io.Reader
	bg      bool
	verbose bool
	onExit  []func() //退出时执行

	readOut func(r io.Reader)
	readErr func(r io.Reader)

	useWindowsLegacyProcessStrategy bool //使用传统模式管理进程(windows)

	onStatusChanged StatusChangedFunc //状态变更时

	err      error           //错误信息
	status   Status          //状态
	pid      PID             //PID
	exitCode int             //退出代码
	sig      os.Signal       //退出信号
	startAt  time.Time       //启动时间
	stopAt   time.Time       //停止时间
	wait     sync.WaitGroup  //等待器
	done     chan struct{}   //已退出信道
	quit     <-chan struct{} //退出信号
}

// StatusChangedFunc 状态变更委托方法
type StatusChangedFunc = func(status Status)

// GetName 命令名称(标识)
func (c *Cmd) GetName() string {
	if c == nil {
		return "<nil>"
	}
	return c.name
}

//String 输出命令行
func (c *Cmd) String() string {
	if c == nil {
		return "<nil>"
	}
	var b strings.Builder
	b.WriteString(c.prefix())
	b.WriteByte(' ')
	b.WriteString(c.path)
	for _, arg := range c.args {
		b.WriteByte(' ')
		if strings.ContainsAny(arg, "\"'` ") {
			b.WriteString(strconv.Quote(arg))
		} else {
			b.WriteString(arg)
		}
	}
	return b.String()
}

// GetOutput 执行命令获取输出
func (c *Cmd) GetOutput() (string, error) {
	var stdOut bytes.Buffer
	var errOut bytes.Buffer

	err := c.StdOut(&stdOut).ErrOut(&errOut).Start()
	if err == nil {
		if errOutput := strings.TrimSpace(errOut.String()); errOutput != "" {
			err = errors.New(errOutput)
		}
	}
	return strings.TrimSpace(stdOut.String()), err
}

// Start 运行
func (c *Cmd) Start() error {
	if c == nil {
		return errors.New("cmd is nil")
	}
	c.reset()

	c.handleStatusChanged(Starting)

	cmd := exec.Command(c.path, c.args...)
	cmd.Dir = c.dir
	cmd.Env = c.env
	cmd.Stdin = c.stdin
	setProcessGroup(cmd, c.useWindowsLegacyProcessStrategy)

	var outRd io.Reader
	if c.readOut != nil {
		if outRd, c.err = cmd.StdoutPipe(); c.err != nil {
			return c.err
		}
	} else {
		cmd.Stdout = c.stdout
	}

	var errRd io.Reader
	if c.readErr != nil {
		if errRd, c.err = cmd.StderrPipe(); c.err != nil {
			return c.err
		}
	} else {
		cmd.Stderr = c.stderr
	}

	c.startAt = time.Now()
	if c.err = cmd.Start(); c.err != nil {
		return c.err
	}

	c.done = make(chan struct{})
	c.pid = PID(cmd.Process.Pid)
	c.handleStatusChanged(Running)

	if outRd != nil && c.readOut != nil {
		c.wait.Add(1)
		go func() {
			defer c.wait.Done()
			c.readOut(outRd)
		}()
	}

	if errRd != nil && c.readErr != nil {
		c.wait.Add(1)
		go func() {
			defer c.wait.Done()
			c.readErr(errRd)
		}()
	}

	return c.waitForExit(cmd)
}

// Stop 停止程序
func (c *Cmd) Stop() error {
	if c != nil {
		c.handleStatusChanged(Stopping)
		return c.pid.stop(c.done)
	}
	return nil
}

// Wait 等待执行完成
func (c *Cmd) Wait() error {
	if c != nil {
		c.wait.Wait()
		return c.err
	}
	return nil
}

// PID 获取命令执行的PID
func (c *Cmd) PID() PID {
	if c == nil {
		return 0
	}
	return c.pid
}

// Status 获取命令执行状态
func (c *Cmd) Status() Status {
	if c == nil {
		return 0
	}
	return c.status
}

// ExitCode 退出代码
func (c *Cmd) ExitCode() int {
	if c == nil {
		return 0
	}
	return c.exitCode
}

// UsedTime 从开始到退出之间的时长
func (c *Cmd) UsedTime() time.Duration {
	if c == nil {
		return 0
	}
	if !c.stopAt.IsZero() {
		return c.stopAt.Sub(c.startAt)
	}
	return time.Since(c.startAt)
}

func (c *Cmd) waitForExit(cmd *exec.Cmd) error {
	if c == nil {
		return nil
	}

	if c.quit != nil {
		c.wait.Add(1)
		go func() {
			defer c.wait.Done()
			select {
			case <-c.done:
			case <-c.quit:
				_ = c.Stop()
			}
		}()
	}

	c.wait.Add(1)
	go func() {
		c.err = cmd.Wait()
		c.stopAt = time.Now()
		c.readExitStatus(cmd)
		c.handleStatusChanged(Stopped)
		close(c.done)
		c.wait.Done()
	}()
	if c.bg {
		return nil
	}
	return c.Wait()
}

func (c *Cmd) readExitStatus(cmd *exec.Cmd) {
	if cmd.ProcessState != nil {
		c.exitCode = cmd.ProcessState.ExitCode()
		if ws, ok := cmd.ProcessState.Sys().(syscall.WaitStatus); ok {
			switch {
			case ws.Stopped():
				c.sig = ws.StopSignal()
			case ws.Signaled():
				c.sig = ws.Signal()
			}
		}
	}
}

func (c *Cmd) handleStatusChanged(status Status) {
	if c == nil {
		return
	}
	c.status = status

	switch status {
	case Starting:
	case Running:
		if c.verbose {
			log.Println(c.String())
		}
	case Stopped:
		if c.verbose {
			if c.err != nil {
				log.Printf("%s %s: %s", c.prefix(), c.UsedTime(), c.err)
			} else {
				log.Printf("%s %s exited: %d", c.prefix(), c.UsedTime(), c.exitCode)
			}
		}
		c.handleExit()
	}

	if c.onStatusChanged != nil {
		c.onStatusChanged(c.status)
	}
}

func (c *Cmd) handleExit() {
	if c == nil {
		return
	}
	for _, fn := range c.onExit {
		if fn != nil {
			fn()
		}
	}
	c.onExit = c.onExit[:0]
}

func (c *Cmd) reset() {
	c.err = nil               //错误信息
	c.status = Ready          //状态
	c.pid = 0                 //PID
	c.exitCode = 0            //退出代码
	c.sig = syscall.Signal(0) //退出信号
	c.startAt = time.Time{}   //启动时间
	c.stopAt = time.Time{}    //停止时间
}

func (c *Cmd) prefix() string {
	if c == nil {
		return ""
	}
	if c.name != "" {
		return fmt.Sprintf("[%d] <%s>", c.pid, c.name)
	}
	return fmt.Sprintf("[%d]", c.pid)
}
